package org.nativejpa.query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nativejpa.util.AnnotationUtils;
import org.nativejpa.util.XMLUtils;
import org.nativejpa.util.ClassUtils;
import org.w3c.dom.Element;

import javax.persistence.NamedNativeQuery;
import javax.persistence.NamedNativeQueries;
import javax.persistence.SqlResultSetMappings;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.NamedQuery;
import javax.persistence.NamedQueries;
import javax.persistence.Entity;
import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;
import java.net.URLClassLoader;
import java.net.URL;

/**
 * Repsitory to hold all NamedSQLTemplate
 *
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-4-2 13:32:57
 */
public class QueryRepository {

    private final static Log log = LogFactory.getLog(QueryRepository.class);

    private final static QueryRepository instance = new QueryRepository();

    /**
     * query name => query template
     */
    private Map<String, NamedSQLTemplate> namedSQLTemplates = new HashMap<String, NamedSQLTemplate>();

    private Map<String, ResultsetMapping> resultsetMappings = new HashMap<String, ResultsetMapping>();


    private QueryRepository() {
    }

    public static QueryRepository getInstance(){
        return instance;
    }

    public Collection<NamedSQLTemplate> getNamedSQLTemplates() {
        return Collections.unmodifiableCollection(namedSQLTemplates.values());
    }

    public NamedSQLTemplate getNamedSQLTemplate(String name) {
        return namedSQLTemplates.get(name);
    }

    public boolean containsNamedSQLTemplate(String name) {
        return namedSQLTemplates.containsKey(name);
    }

    public ResultsetMapping getResultsetMapping(String name) {
        return resultsetMappings.get(name);
    }

    public boolean containsResultsetMapping(String name) {
        return resultsetMappings.containsKey(name);
    }

    public Collection<ResultsetMapping> getResultsetMappings() {
        return resultsetMappings.values();
    }

    public void parseEntities(Class<?>[] entityClasses) {
        for(Class<?> entityClass : entityClasses) {
            parseSqlResultsetMapping(entityClass);
            parseNamedNativeQuery(entityClass);
            if(entityClass.isAnnotationPresent(NamedQueries.class) || entityClass.isAnnotationPresent(NamedQuery.class)) {
                log.warn("Such Classes annotated by @NamedQuery/@NamedQueries, please use @NamedNativeQuery/@NamedNativeQueries instead, " + entityClass.getName());
            }
        }
    }

    private void parseNamedNativeQuery(Class<?> entityClass){
            log.info("Parsing @NamedNativeQuery from: " + entityClass.getName());
            List<String> queryNames = new ArrayList<String>();
            if (entityClass.isAnnotationPresent(NamedNativeQueries.class)) {
                NamedNativeQueries namedNativeQueries = entityClass.getAnnotation(NamedNativeQueries.class);
                for (NamedNativeQuery namedNativeQuery : namedNativeQueries.value()) {
                    NamedSQLTemplate sqlTemplate = new NamedSQLTemplate(namedNativeQuery, entityClass);
                    log.debug("Created NamedNatvieQuery: " + sqlTemplate);
                    this.registerNamedSQLTemplate(sqlTemplate);
                    queryNames.add(namedNativeQuery.name());
                }
            }
            if (entityClass.isAnnotationPresent(NamedNativeQuery.class)) {
                NamedNativeQuery namedNativeQuery = entityClass.getAnnotation(NamedNativeQuery.class);
                NamedSQLTemplate sqlTemplate = new NamedSQLTemplate(namedNativeQuery, entityClass);
                log.debug("Created NamedNatvieQuery: " + sqlTemplate);
                this.registerNamedSQLTemplate(sqlTemplate);
                queryNames.add(namedNativeQuery.name());
            }
            log.info("Register NamedQuery for Class: " + entityClass.getName() + ", " + Arrays.toString(queryNames.toArray(new String[queryNames.size()])));
    }

    public void registerNamedSQLTemplate(NamedSQLTemplate sqlTemplate) {
        if(containsNamedSQLTemplate(sqlTemplate.getName())) {
            log.warn("NamedQuery " + sqlTemplate.getName() + " already exsists which was registered in " + namedSQLTemplates.get(sqlTemplate.getName()).getDefinedClass() + ", will overwrite existed one");
        }
        namedSQLTemplates.put(sqlTemplate.getName(), sqlTemplate);
    }

    private void parseSqlResultsetMapping(Class<?> entityClass) {
            log.info("Parsing @SqlResultsetMapping from: " + entityClass.getName());
            if (entityClass.isAnnotationPresent(SqlResultSetMappings.class)) {
                SqlResultSetMappings sqlResultSetMappings = entityClass.getAnnotation(SqlResultSetMappings.class);
                for(SqlResultSetMapping sqlResultSetMapping : sqlResultSetMappings.value()){
                    ResultsetMapping resultsetMapping = new ResultsetMapping(sqlResultSetMapping);
                    log.debug("Created SqlResultsetMapping: " + resultsetMapping);
                    registerSqlResultsetMapping(resultsetMapping);
                }
            }
            if (entityClass.isAnnotationPresent(SqlResultSetMapping.class)) {
                SqlResultSetMapping sqlResultSetMapping = entityClass.getAnnotation(SqlResultSetMapping.class);
                ResultsetMapping resultsetMapping = new ResultsetMapping(sqlResultSetMapping);
                log.debug("Created SqlResultsetMapping: " + resultsetMapping);
                registerSqlResultsetMapping(resultsetMapping);
            }
    }

    public void registerSqlResultsetMapping(ResultsetMapping resultsetMapping){
        if(resultsetMappings.containsKey(resultsetMapping.getName())){
            log.warn("SqlResultsetMapping " + resultsetMapping.getName() + " already exists, will overwrite existed one.");
        }
        resultsetMappings.put(resultsetMapping.getName(), resultsetMapping);
    }

    public void parseMappingFiles(String[] mappingFiles){
        for(String mappingFile : mappingFiles){
            log.info("Parsing mapping file: " + mappingFile);
            URL mappingFileURL = this.getClass().getClassLoader().getResource(mappingFile);
            if(mappingFileURL == null) {
                log.warn("Can not find mapping file: " + mappingFile);
            }
            else {
                try {
                    parseMappingFile(mappingFileURL);
                }
                catch (Exception e) {
                    log.warn("Failed to parse maping file: " + mappingFile, e);
                }
            }
        }
    }

    // Only parse NamedNativeQuery and SqlResultsetMapping
    private void parseMappingFile(URL ormXML) throws Exception {
        //parseNamedNativeQuery
        Element rootElement = XMLUtils.loadDocument(ormXML).getDocumentElement();
        List<Element> entityElements = XMLUtils.getElementsByTagName(rootElement, "entity");
        for(Element entityElement : entityElements) {
            String entityClassName = XMLUtils.getAtrributeValue(entityElement, "class");
            Class<?> entityClass = ClassUtils.getClass(entityClassName);
            List<Element> namedNativeQueryElements = XMLUtils.getChildElementsByTagName(entityElement,"named-native-query");
            for(Element namedNativeQueryElement : namedNativeQueryElements){
                Class<?> resultClass = void.class; // default
                String resultClassName = XMLUtils.getAtrributeValue(namedNativeQueryElement, "result-class");
                if(resultClassName != null) {
                    resultClass = ClassUtils.getClass(resultClassName);
                }
                NamedSQLTemplate namedSQLTemplate = new NamedSQLTemplate(namedNativeQueryElement, resultClass, entityClass);
                //register, will overwrite exsit on
                log.debug("Created NamedNatvieQuery: " + namedSQLTemplate);
                registerNamedSQLTemplate(namedSQLTemplate);
            }

            //parse tag resultset-mapping
            List<Element> sqlResultsetMappingElements = XMLUtils.getChildElementsByTagName(entityElement,"sql-result-set-mapping");
            for(Element sqlResultsetMappingElement : sqlResultsetMappingElements){
                ResultsetMapping resultsetMapping = new ResultsetMapping(sqlResultsetMappingElement);
                //register, will overwrite exsit on
                log.debug("Created SqlResultsetMapping: " + resultsetMapping);
                registerSqlResultsetMapping(resultsetMapping);
            }
        }
    }
}
